package jacky.lanlan.song.jdbc;

import java.lang.reflect.Method;
import java.sql.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.sql.DataSource;

import org.apache.log4j.Logger;

import jacky.lanlan.song.closure.Handler;
import jacky.lanlan.song.reflection.ReflectionUtils;
import jacky.lanlan.song.reflection.ReflectionUtils.MethodCallback;
import jacky.lanlan.song.reflection.ReflectionUtils.MethodFilter;
import jacky.lanlan.song.reflection.bean.JavaBeanUtils;
import jacky.lanlan.song.util.Assert;

/**
 * 操作纯JDBC API时的帮助类，可以和 {@link JdbcUtils} 配合使用。
 * <p>
 * 该类所有的方法都是辅助使用JDBC API的，简单包装了一下，可以适当简化代码，
 * 如果持久层框架提供了所需功能则不需要使用。
 * @author Jacky.Song
 */
public class RawJDBCHelper {

	private DataSource dataSource = null;
  
  private static final Logger logger=Logger.getLogger(RawJDBCHelper.class);
  
  /*
   * 反射ResultSet所有的带一个String参数的get方法
   * Map 映射: [getter返回值类型 -> getter方法]
   * 
   * 反射出的方法有23个，所以初始化容量设置为23个，32*0.75=23
   */
  private static Map<Class<?>,Method> rsGetters=new ConcurrentHashMap<Class<?>,Method>(32);
  
  static{
  	//通过反射建立rsGetter映射
  	ReflectionUtils.doWithMethods(ResultSet.class, new MethodCallback() {
			public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
				rsGetters.put(
						method.getReturnType()==Timestamp.class?java.util.Date.class:method.getReturnType(),
						method);
				/*
				 * 上面做了个转换，把getTimestamp()方法映射到java.util.Date类型，这样接收java.util.Date类型参数的方法，
				 * 就可以接收RS的getTimestamp()方法的返回值
				 */
			}
  	}, 
  	new MethodFilter(){
			public boolean matches(Method method) {
	  		return 
	  			MethodFilter.PUBLIC.matches(method) && 
	  			method.getParameterTypes().length==1 && // 1个参数
	  			method.getName().startsWith("get") // get开头 
	  			&& method.getParameterTypes()[0]==String.class; //参数类型为String
			}
  	});
	}
  
  /////////依赖注入///////////
	/**
	 * 注入数据源。<br>
	 * 该数据源将被提供给getConnection()方法，以获取连接。
	 * @param source
	 */
	public void setDataSource(DataSource source) {
		this.dataSource = source;
	}
	//////////////////////////

	/**
	 * @return 该Helper使用的数据源
	 */
	public DataSource getDataSource() {
		return this.dataSource;
	}
	
	/**
	 * 得到注入数据源的一个数据库连接。
	 * <p>
	 * 任何异常都将转换为RuntimeException后抛出。
	 * @return 一个可用的数据库连接
	 */
  public Connection getConnection(){
    try {
			return this.dataSource.getConnection();
		}
		catch (SQLException e) {
			logger.error("得到连接时出错：",e);
			throw new RuntimeException("得到连接时出错",e);
		}
  }

  /**
	 * 处理DML语句。
	 * <p>
	 * 该方法包装了DML语句的处理，通过一个回调接口(Handler)暴露语句的设置逻辑，
	 * 客户端只需要指定DML语句以及处理语句的设置逻辑即可，该方法自动处理得到连接、语句创建、
	 * 关闭连接等事务。
	 * <p>
	 * 处理逻辑中的任何异常都将包装为RuntimeException抛出。
	 * <br>
	 * 该方法将返回PreparedStatement.executeUpdate()方法的返回值。
	 * 客户端可以通过StatementSetter完全控制DML语句的设置，
	 * <b style="color:red">不需要写try/catch块，只需提供 Handler 实现即可</b>。
	 * @see Handler
	 * @param sql 要执行的sql语句
	 * @param stmtSetter PreparedStatement的设置逻辑
	 * @return 因执行sql而被影响的记录数
	 */
  public int handleDML(String sql, Handler<PreparedStatement> stmtSetter) {
  	int returnValue=0;
		Connection con=null;
		try {
			con=this.getConnection();
			PreparedStatement stmt =null;
			try {
				stmt = con.prepareStatement(sql);
				stmtSetter.doWith(stmt);
				returnValue=stmt.executeUpdate();
			}
			finally {JdbcUtils.closeStatement(stmt);}
		}
		catch (SQLException e) {
			logger.error("处理语句 ["+sql+"] 时出错："+e);
			throw new RuntimeException("处理语句 ["+sql+"] 时出错："+e);
		}
		finally {
			JdbcUtils.closeConnection(con);
		}
		return returnValue;
	}

  /**
   * 处理存储过程。
   * <p>
   * 该方法包装了存储过程的处理，通过一个回调接口(ProcedureHandler)暴露具体的处理逻辑，
   * 客户端只需要指定存储过程以及处理逻辑即可，该方法自动处理得到连接、语句创建、关闭
   * 连接等事务。
   * <p>
   * 处理逻辑中的任何异常都将包装为RuntimeException抛出。
   * <br>
   * 该方法将返回ProcedureHandler.afterExecute()方法的reValues参数，所以客户端可以得到存储过程的返回值，
   * 像这样：
   * <code><pre>
   * Map<Integer, Object> reValues=this.handleProcedure("sql",new new ProcedureHandler(){...});
   * value1=reValues.get(1);
   * value2=reValues.get(2);
   * value3=reValues.get(3);
   * </pre></code>
   * 客户端可以通过ProcedureHandler完全控制存储过程的处理，以及得到返回值，该方法不会对处理逻辑产生任何影响。所以
   * <b style="color:red">不需要写try/catch块，只需提供 ProcedureHandler 实现即可</b>。
   * @see ProcedureHandler
   * @param sql 调用存储过程的sql语句
   * @param handler 处理逻辑
   * @return 装有返回值的Map
   */
  public Map<Integer, Object> handleProcedure(String sql, ProcedureHandler handler) {
  	Map<Integer,Object> rsValues=new HashMap<Integer, Object>(6,0.5f);//默认容量为3，节约内存
  	Connection con=null;
  	try {
  		con=this.getConnection();
  		CallableStatement proc =null;
  		try {
  			proc = con.prepareCall(sql);
  			ResultSet rs=null;
  			try {
  				handler.beforeExecute(proc);
  				proc.execute();
  				rs=handler.afterExecute(proc,rsValues);
  			}
  			finally {JdbcUtils.closeResultSet(rs);}
  		}
  		finally {JdbcUtils.closeStatement(proc);}
  	}
  	catch (SQLException e) {
  		logger.error("处理存储过程 ["+sql+"] 时出错："+e);
  		throw new RuntimeException("处理存储过程 ["+sql+"] 时出错："+e);
  	}
  	finally {
  		JdbcUtils.closeConnection(con);
  	}
  	return rsValues;
  }
  
  /**
   * 用反射动态填充域对象。
   * <p>
   * 这个方法通过反射把ResultSet中的值填充到域对象中(通过反射调用其setter)，并以List方式返回。<br>
   * <b style="color:red">注意：该方法默认调用那些名称和数据库表的字段名相同的Field的setter方法，来设置值。</b><br>
   * 如果有不同名称需要映射(比如把addr映射为address)，则需要提供名称映射表(Key=Field名，Value=表字段名(忽略大小写))。<br>
   * 只需要映射不匹配的Field和表字段名，未指定映射的字段将以默认方式赋值，若不想反射某项Field，设置其Value为null。
   * @param <T> 域对象类型
   * @param domainClass 域对象Class
   * @param rs 从数据库取的ResultSet
   * @param nameFieldMap [域对象Field名 --> 数据库表字段名] 映射
   * @return 处理完成后的List
   * @throws SQLException 
   */
  public <T> List<T> fillDomain(Class<T> domainClass,ResultSet rs,
  		Map<String,String> nameFieldMap) throws SQLException {
  	Assert.notNull(rs, "结果集不能为空");
  	List<T> list=new ArrayList<T>();
  	StringBuilder setterName=new StringBuilder();//因为要频繁操作字符串
		while (rs.next()) {
			T domain=this.reflectData(domainClass,setterName,rs,nameFieldMap);
			list.add(domain);
		}
		setterName=null;
  	return list;
  }
  /*
   * 创建一个域对象，把一行ResultSet数据通过反射填充到该对象，然后返回。
   */
  private <T> T reflectData(Class<T> domainClass,StringBuilder setterName,
  		ResultSet rs,Map<String,String> nameFieldMap) {
  	T domain=null;
		try {
			domain = domainClass.newInstance();
		}
		catch (Exception e) {
			logger.error("反射创建"+domainClass.getSimpleName()+"时出错",e);
			throw new RuntimeException("反射创建"+domainClass.getSimpleName()+"时出错");
		}
		String fieldName=null;//反射出的带 setter 的Field名
		Class<?> [] paramTypes=null;
		Method errorInvoke=null;//调用失败的方法
		Object rsReturnValue=null;
		try {
			for(Method setter:JavaBeanUtils.extractSetter(domainClass)) {
				errorInvoke=setter;
				paramTypes=setter.getParameterTypes();//方法参数类型
				if(rsGetters.containsKey(paramTypes[0])) {//如果查到了对应该参数类型的RS方法
					setterName.append(setter.getName());
					fieldName=parseFieldName(setterName);//解析出field名
					fieldName=(nameFieldMap!=null && nameFieldMap.containsKey(fieldName))?//如果有映射，则替换为新的字段名字
							nameFieldMap.get(fieldName):fieldName;
					if(fieldName!=null) {//忽略null值(null代表不设置此属性)
						rsReturnValue = rsGetters.get(paramTypes[0]).invoke(rs,//提取对应方法参数类型的RS方法并调用，得到数据库数据
								fieldName.toLowerCase());//传递的参数即是field名(小写)
						setter.invoke(domain,rsReturnValue);//反射调用domain的setter方法，把值赋给domain
					}
				}
				setterName.delete(0, setterName.length());//清空，开始下一个方法
			}
		}
		catch (Exception e) {
			logger.error("反射调用"+errorInvoke.getName()+"时出错",e);
		}
		return domain;
  }
  //setterName是StringBuilder，直接解析也很方便，所以没有用JavaBeanUtils的方法来解析属性名
  private static String parseFieldName(StringBuilder setterName) {
  	setterName.delete(0, 3);//去掉set
  	setterName.setCharAt(0, 
  			Character.toLowerCase(setterName.charAt(0)));//头字母小写，转换为对应的field名
    return setterName.toString();
  }

}
